﻿//Allen Jordan, 2013
//This program is under the MIT License:  http://opensource.org/licenses/mit-license.php

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
//using System.Web;
using System.IO;
using System.Net;
using Newtonsoft.Json;
using MathNet.Numerics;
using MathNet.Numerics.Statistics;
//using ZedGraph;
using System.Configuration;
using System.Security;

namespace dogetrader
{
    public partial class Form1 : Form
    {
        private int updatePeriodSeconds;

        private BackgroundWorker backgroundWorker;
        private ZedGraph.RollingPointPairList rollingPointPairPriceList, rollingPointPairVolumeList;
        private ZedGraph.PointPairList restoredPointPairPriceList, restoredPointPairVolumeList;
        
        private int xRangeMinutes = 15;
        private const int graphBufferCapacityDays = 3;
        private const int graphBufferCapacityPoints = graphBufferCapacityDays * 24 * 60 * 60 / 5;//about 3 days worth of trades



        private FileStream receivedTradesLogFileStream = null;
        private StreamWriter statusLogWriter, receivedTradesLogWriter;

        //the time zone of the Cryptsy server
        private TimeZoneInfo cryptsyServerTimeZoneInfo;

        //private double dogecoins = double.NaN;
        //private double bitcoins = double.NaN;

        //the latest-trade price of doge (in btc)
        private double latestDOGEPriceInBTC = double.NaN;
        //the current btc and doge balances on Cryptsy
        private double btcAvailable, dogeAvailable;
        //the current Cryptsy buy/sell DOGE fees
        //todo: actually read these from the server (currently they are never changed)
        private double dogeBTCFee = 0.3;//selling DOGE fee
        private double btcDogeFee = 0.2;//buying DOGE fee

        public Form1()
        {
            //make sure the culture settings are always the same, not set to the computer locale
            System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;

            InitializeComponent();

            cryptsyServerTimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
            //var path = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;

            try
            {
                //make sure the properties.settings area is encrypted
                Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
                ConfigurationSection section = config.GetSection("userSettings/dogetrader.Properties.Settings");
                if (section != null)
                {
                    //if we have not already set the section protection...
                    if (!section.SectionInformation.IsProtected)
                    {
                        if (!section.ElementInformation.IsLocked)
                        {
                            //Uses the Windows Data Protection API (DPAPI) to encrypt and decrypt data
                            //http://msdn.microsoft.com/en-US/library/68ze1hb2%28v=vs.80%29.aspx
                            section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                            section.SectionInformation.ForceSave = true;
                            config.Save(ConfigurationSaveMode.Full);
                        }
                        else throw new Exception("Configuration section is locked");
                    }
                }
                else throw new Exception("Invalid configuration section");
            }
            catch (Exception ex)
            {
                MessageBox.Show("Error encrypting configuration settings (for safe API key storage): " + ex.ToString());
                this.Close();
                return;
            }

            //setup status message / received trades logging
            try
            {
                string logDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + Path.DirectorySeparatorChar + "dogetrader";
                DirectoryInfo logDirInfo = new DirectoryInfo(logDirectory);
                if (!logDirInfo.Exists) logDirInfo.Create();
                statusLogWriter = new StreamWriter(new FileStream(logDirInfo.FullName + Path.DirectorySeparatorChar + "statuslog.txt", FileMode.Create, FileAccess.Write));
                receivedTradesLogFileStream = new FileStream(logDirInfo.FullName + Path.DirectorySeparatorChar + "receivedTrades.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
            }
            catch (Exception)
            {
                statusLogWriter = null;
                receivedTradesLogWriter = null;
                toolStripStatusLabel1.Text = "Could not create status message log";
            }

            updatePeriodSeconds = Properties.Settings.Default.updatePeriodSeconds;

            //roughly 5 seconds between transactions, so make a huge circular buffer that will last about 3 days
            rollingPointPairPriceList = new ZedGraph.RollingPointPairList(graphBufferCapacityPoints);
            rollingPointPairVolumeList = new ZedGraph.RollingPointPairList(rollingPointPairPriceList.Capacity);
            restoredPointPairPriceList = new ZedGraph.PointPairList();
            restoredPointPairVolumeList = new ZedGraph.PointPairList();

            //setup ZedGraph options:

            //BTC per 1 DOGE price curve
            ZedGraph.LineItem curve = zedGraphControl1.GraphPane.AddCurve("doge price", rollingPointPairPriceList, Color.Blue);
            curve.Symbol.IsVisible = false;
            //curve.Symbol.Type = ZedGraph.SymbolType.Circle;
            //curve.Symbol.Fill = new ZedGraph.Fill(Color.Blue);
            //curve.Symbol.Size = 5;
            curve.Line.IsVisible = true;
            curve.Line.Width = 4;

            //a separate curve for any price data restored from log file on program load (to prevent lines over data gaps)
            ZedGraph.LineItem curve2 = zedGraphControl1.GraphPane.AddCurve("doge price (restored from log)", restoredPointPairPriceList, Color.Blue);
            curve2.Symbol.IsVisible = false;
            curve2.Line.IsVisible = true;
            curve2.Line.Width = 4;

            //DOGE volume bar series
            zedGraphControl1.GraphPane.BarSettings.ClusterScaleWidth = 0.004;
            Color barColor = Color.FromArgb(255, 230, 230);
            ZedGraph.BarItem bar = zedGraphControl1.GraphPane.AddBar("doge volume", rollingPointPairVolumeList, barColor);
            bar.IsY2Axis = true;
            bar.Bar.Fill = new ZedGraph.Fill(barColor);
            bar.Bar.Border.IsVisible = false;

            //a separate bar series for any volume data restored from log file on program load
            ZedGraph.BarItem bar2 = zedGraphControl1.GraphPane.AddBar("doge volume (restored from log)", restoredPointPairVolumeList, barColor);
            bar2.IsY2Axis = true;
            bar2.Bar.Fill = new ZedGraph.Fill(barColor);
            bar2.Bar.Border.IsVisible = false;

            //axis/legend settings
            zedGraphControl1.GraphPane.Legend.IsVisible = false;
            zedGraphControl1.GraphPane.Title.IsVisible = false;
            zedGraphControl1.GraphPane.XAxis.Title.Text = "Time";
            zedGraphControl1.GraphPane.XAxis.Type = ZedGraph.AxisType.Date;
            zedGraphControl1.GraphPane.YAxis.Title.Text = "DOGE Price [BTC]";
            zedGraphControl1.GraphPane.Y2Axis.IsVisible = true;
            zedGraphControl1.GraphPane.Y2Axis.Title.Text = "Volume";

            //scale the X axis for the user-selected time span
            UpdateGraphXAxisForTrailingMin();
            GraphAxisChangeAndRefresh(true, true);

            //start a worker thread for fetching online DOGE information
            backgroundWorker = new BackgroundWorker();
            backgroundWorker.DoWork += DoWork;
            backgroundWorker.WorkerSupportsCancellation = true;
            backgroundWorker.RunWorkerAsync();
        }

        private delegate void ShowMessageDelegate(string messageShort, string messageLong, bool logToFile);
        /**
         * Show and log a status message (thread safe).  
         */
        private void SetStatusMessage(string messageShort, string messageLong, bool logToFile)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new ShowMessageDelegate(SetStatusMessage), new object[] {messageShort, messageLong, logToFile});
            }
            else
            {
                if (logToFile)
                {
                    DateTime now = DateTime.Now;
                    string dateTimeStamp = "[" + now.ToShortDateString() + " " + now.ToLongTimeString() + "] ";
                    if (statusLogWriter != null) statusLogWriter.WriteLine(dateTimeStamp + messageShort + "\r\n\r\n" + messageLong + "\r\n");
                }

                toolStripStatusLabel1.Text = messageShort;
            }
        }

        private void SetProgress(int percent)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new CryptsyAPI.APIMethods.SetProgressDelegate(SetProgress), new object[] { percent });
            }
            else
            {
                toolStripProgressBar1.Value = percent;

                if (percent == 100)
                {
                    progressClearingTimer.Start();
                }
            }
        }

        /**
         * Used to automatically clear the progress bar after it reaches 100%
         */
        private void progressClearingTimer_Tick(object sender, EventArgs e)
        {
            progressClearingTimer.Stop();
            toolStripProgressBar1.Value = 0;
        }

        private DateTime prevVolumeDateTimeLocal = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Local);
        private double volumeTotal = 0;
        private delegate void UpdatePriceDelegate(DateTime dateTimeUTC, double dogePrice, double dogeQuantity, bool isRestoredFromFile);
        private void UpdateDOGEPriceQuantity(DateTime dateTimeUTC, double dogePrice, double dogeQuantity)
        {
            UpdateDOGEPriceQuantity(dateTimeUTC, dogePrice, dogeQuantity, false);
        }
        /**
         * Update the GUI with new DOGE price/volume information (thread-safe).  
         */
        private void UpdateDOGEPriceQuantity(DateTime dateTimeUTC, double dogePrice, double dogeQuantity, bool isRestoredFromFile)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new UpdatePriceDelegate(UpdateDOGEPriceQuantity), new object[] { dateTimeUTC, dogePrice, dogeQuantity, isRestoredFromFile });
            }
            else
            {
                latestDOGEPriceInBTC = dogePrice;

                DateTime dateTimeLocal = TimeZoneInfo.ConvertTimeFromUtc(dateTimeUTC, TimeZoneInfo.Local);
                ZedGraph.IPointListEdit priceList = rollingPointPairPriceList;
                ZedGraph.IPointListEdit volumeList = rollingPointPairVolumeList;
                if (isRestoredFromFile)
                {
                    priceList = restoredPointPairPriceList;
                    volumeList = restoredPointPairVolumeList;
                }

                //initialize the prevVolumeDateTime if necessary
                if (prevVolumeDateTimeLocal.Year == 2000)
                {
                    prevVolumeDateTimeLocal = dateTimeLocal;
                }

                //calculate volume (summed quantities on an interval)
                volumeTotal += dogeQuantity;
                if ((dateTimeLocal - prevVolumeDateTimeLocal).TotalMinutes >= 4)
                {
                    //set the datetime for plotting to the middle of the summing period
                    DateTime middleDateTime = dateTimeLocal.AddSeconds(-1 * (dateTimeLocal - prevVolumeDateTimeLocal).TotalSeconds / 2);
                    //update the plot with the summed volume
                    volumeList.Add(middleDateTime.ToOADate(), volumeTotal);
                    volumeTotal = 0;
                    prevVolumeDateTimeLocal = dateTimeLocal;
                }

                priceList.Add(dateTimeLocal.ToOADate(), dogePrice);
                
                UpdateGraphXAxisForTrailingMin();

                btcDogePriceLabel.Text = String.Format("DOGE Price: {0:0.00000000} [BTC]", dogePrice);
                buyPriceDogeTextBox.Text = String.Format("{0:0.00000000}", dogePrice);
                sellPriceDogeTextBox.Text = String.Format("{0:0.00000000}", dogePrice);
            }
        }

        private delegate void UpdateBalancesDelegate(double btcAvailable, double dogeAvailable);
        /**
         * Update the user's Cryptsy BTC and DOGE balances on the GUI (thread-safe).  
         */
        private void UpdateBalances(double btcAvailable, double dogeAvailable)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new UpdateBalancesDelegate(UpdateBalances), new object[] { btcAvailable, dogeAvailable });
            }
            else
            {
                this.btcAvailable = btcAvailable;
                this.dogeAvailable = dogeAvailable;

                btcAvailableLabel.Text = string.Format("{0:0.00000000} [BTC]", btcAvailable);
                dogeAvailableLabel.Text = string.Format("{0:0.00000000} [DOGE]", dogeAvailable);

                sellAmountDogeTextBox.Text = string.Format("{0:0.00000000}", dogeAvailable);
                buyAmountDogeTextBox.Text = string.Format("{0:0.00000000}", btcAvailable / latestDOGEPriceInBTC - (btcDogeFee / 100.0) * (btcAvailable / latestDOGEPriceInBTC));
            }
        }
        

        private delegate void SetUpdatedDateTimeDelegate(DateTime updatedDateTimeLocal);
        /**
         * Update the date/time of latest data update/retrieval (thread-safe)
         */
        private void SetUpdatedDateTime(DateTime updatedDateTimeLocal)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new SetUpdatedDateTimeDelegate(SetUpdatedDateTime), new object[] { updatedDateTimeLocal });
            }
            else
            {
                updatedLabel.Text = "Updated: " + updatedDateTimeLocal.ToLongTimeString();
            }
        }

        private CryptsyAPI.DMDRecentTradeComparer tradeComparer = new CryptsyAPI.DMDRecentTradeComparer();
        private HashSet<CryptsyAPI.DMDRecentTrade> previousTradeSet;
        /**
         * Retrieve recent DOGE/BTC trade info from Cryptsy, discard trades we already know, and update the plot/data.  
         * note: be sure previousTradeSet is already initialized
         * note: this is tightly coupled to the graph data, so moving it to APIMethods.cs is difficult
         */
        private void FetchAndUpdateDOGETrades()
        {
            string cryptsyDogeBTCRequest = @"http://pubapi.cryptsy.com/api.php?method=singlemarketdata&marketid=132";

            HashSet<CryptsyAPI.DMDRecentTrade> recentTradeSet = new HashSet<CryptsyAPI.DMDRecentTrade>(tradeComparer);

            //fetch data from Cryptsy
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(cryptsyDogeBTCRequest);
            StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream());
            string jsonStr = reader.ReadToEnd();

            //parse the JSON result from Cryptsy
            CryptsyAPI.DogeMarketData dogeMarketData = JsonConvert.DeserializeObject<CryptsyAPI.DogeMarketData>(jsonStr);

            //find any new transactions and update the GUI with their information
            for (int i = dogeMarketData.@return.markets.DOGE.recenttrades.Length - 1; i >= 0; i--)
            {
                CryptsyAPI.DMDRecentTrade trade = dogeMarketData.@return.markets.DOGE.recenttrades[i];
                recentTradeSet.Add(trade);

                if (!previousTradeSet.Contains(trade))
                {
                    //convert the trade date/time to UTC
                    trade.time = TimeZoneInfo.ConvertTimeToUtc(trade.time, cryptsyServerTimeZoneInfo);
                    UpdateDOGEPriceQuantity(trade.time, trade.price, trade.quantity);

                    string tradeJSON = JsonConvert.SerializeObject(trade);
                    receivedTradesLogWriter.WriteLine(tradeJSON);
                }
            }

            GraphAxisChangeAndRefresh(true, true);

            receivedTradesLogWriter.Flush();

            previousTradeSet = recentTradeSet;
        }

        /**
         * Worker thread entry point for retrieving DOGE information online, doing various calculations, etc.  
         */
        private void DoWork(object obj, DoWorkEventArgs args)
        {
            //make sure the culture settings are always the same, not set to the computer locale
            System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;

            //initialize the previous transaction storage
            previousTradeSet = new HashSet<CryptsyAPI.DMDRecentTrade>(tradeComparer);

            Cursor.Current = Cursors.WaitCursor;

            //if there are any logged previous transactions, load them from file and update the GUI
            //note: this will remove any old log entries, then re-write the log file to avoid it growing indefinitely
            if (receivedTradesLogFileStream != null)
            {
                SetProgress(10);

                DateTime thresholdDateTime = DateTime.UtcNow.Subtract(TimeSpan.FromDays(graphBufferCapacityDays));

                //first read all of the trades into memory
                StreamReader reader = new StreamReader(receivedTradesLogFileStream);
                while (!reader.EndOfStream)
                {
                    try
                    {
                        string jsonTrade = reader.ReadLine();
                        CryptsyAPI.DMDRecentTrade trade = JsonConvert.DeserializeObject<CryptsyAPI.DMDRecentTrade>(jsonTrade);

                        //only accept recent trades
                        if (trade.time >= thresholdDateTime)
                        {
                            previousTradeSet.Add(trade);
                        }
                    }
                    catch (Exception ex)
                    {
                        SetStatusMessage("Error reading logged trade data: " + ex.Message, ex.ToString(), true);
                    }
                }

                SetProgress(50);

                //clear the contents of the received trades log file
                receivedTradesLogFileStream.SetLength(0);
                receivedTradesLogFileStream.Flush();

                //open the received trades log file for writing new entries
                receivedTradesLogWriter = new StreamWriter(receivedTradesLogFileStream);

                //for each of the received trades that passed the datetime check, write it to the newly-emptied log file and update the GUI graph/display/etc
                foreach (CryptsyAPI.DMDRecentTrade trade in previousTradeSet)
                {
                    string tradeJSON = JsonConvert.SerializeObject(trade);//todo: this is a little redundant... maybe cache JSON string when read earlier?
                    receivedTradesLogWriter.WriteLine(tradeJSON);

                    UpdateDOGEPriceQuantity(trade.time, trade.price, trade.quantity, true);
                }
                GraphAxisChangeAndRefresh(true, true);

                SetProgress(90);
            }

            

            //if we have valid API keys, fetch the initial Cryptsy coin balances
            if (Properties.Settings.Default.publicAPIKey != "blah1")
            {
                try
                {
                    double btc, doge;
                    CryptsyAPI.APIMethods.CryptsyGetInfo(out btc, out doge, SetProgress);
                    UpdateBalances(btc, doge);
                }
                catch (Exception ex)
                {
                    SetStatusMessage("Could not fetch Cryptsy balances (check API keys): " + ex.Message, ex.ToString(), true);
                }
            }

            System.Threading.Thread.Sleep(500);

            try
            {
                //find any initial transactions and update the GUI
                FetchAndUpdateDOGETrades();
                SetUpdatedDateTime(DateTime.UtcNow);
            }
            catch (Exception ex)
            {
                SetStatusMessage("Failed initial Cryptsy retrieval: " + ex.Message, ex.ToString(), true);
            }

            //int listMaxSize = (10 * 60 * 1000) / updatePeriodMS;

            Cursor.Current = Cursors.Default;

            DateTime previousUpdateDateTimeUTC = DateTime.UtcNow;
            bool done = false;
            while (!done)
            {
                try
                {
                    if ((DateTime.UtcNow - previousUpdateDateTimeUTC).TotalSeconds >= updatePeriodSeconds)
                    {
                        //retrieve any new DOGE trade prices/volumes and update the GUI for plotting/display
                        FetchAndUpdateDOGETrades();

                        DateTime updatedDateTimeUTC = DateTime.UtcNow;
                        //show the local time of this latest update on the GUI for indication that the thread is working, even if data hasn't changed
                        SetUpdatedDateTime(updatedDateTimeUTC);
                        previousUpdateDateTimeUTC = updatedDateTimeUTC;

                        //since everything was successful to this point, clear any previous status/error messages
                        SetStatusMessage("", "", false);

                        /*dogePriceList.AddLast(dogePrice);
                        if (dogePriceList.Count > listMaxSize) dogePriceList.RemoveFirst();
                        double mean = Statistics.Mean(dogePriceList);
                        double max = Statistics.Maximum(dogePriceList);
                        double min = Statistics.Minimum(dogePriceList);
                        double delta = max - min;

                        if (delta >= 0.00000002)
                        {

                            double buyThreshold = delta * .2 + min;
                            double sellThreshold = delta * .8 + min;
                            if (dogePrice <= buyThreshold)
                            {
                                //buy
                                dogecoins = (1.0 / dogePrice * bitcoins) - btcDogeFee / 100.0 * (1.0 / dogePrice * bitcoins);
                                bitcoins = 0;

                                ShowMessage(string.Format("buy  d = {0:0.0000} b = {1:0.000000000}", dogecoins, bitcoins));
                            }
                            else if (dogePrice >= sellThreshold)
                            {
                                //sell
                                bitcoins = (dogePrice * dogecoins) - dogeBTCFee / 100.0 * (dogePrice * dogecoins);
                                dogecoins = 0;

                                ShowMessage(string.Format("sell  d = {0:0.0000} b = {1:0.000000000}", dogecoins, bitcoins));
                            }

                        }
                        */
                        //ShowMessage(string.Format("Dogecoin Price: {0:0.000000000}\r\n", dogePrice));

                        
                    }//end of updateperiod datetime check

                    //small delay to keep things responsive
                    System.Threading.Thread.Sleep(100);

                    //see if the GUI has requested thread close/cancel
                    done = backgroundWorker.CancellationPending;
                }//end of try
                catch (Exception ex)
                {
                    SetStatusMessage("Online fetching thread error: " + ex.Message, ex.ToString(), true);
                }
            }//end of while!done
        }//end of DoWork method

        /**
         * Clean up before closing the window.  
         */
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            //stop the worker thread
            if (backgroundWorker.WorkerSupportsCancellation == true) 
                backgroundWorker.CancelAsync();
            //flush/close the log file writer
            if (statusLogWriter != null) statusLogWriter.Close();
        }

        /**
         * This uses the shared xRangeMinutes value to adjust the graph's X scale minimum.  Note: this does not call axischange or refresh... use GraphAxisChangeAndRefresh afterwards for this.  
         */
        private void UpdateGraphXAxisForTrailingMin()
        {
            if (this.InvokeRequired) throw new Exception("Can't update graph from worker threads directly... use thread-safe call");

            DateTime maxDateTime = DateTime.Now;
            DateTime minDateTime = maxDateTime;
            if (rollingPointPairPriceList.Count > 0)
            {
                maxDateTime = DateTime.FromOADate(rollingPointPairPriceList[rollingPointPairPriceList.Count - 1].X);
                minDateTime = maxDateTime.Subtract(TimeSpan.FromMinutes(xRangeMinutes));
            }
            else
                minDateTime = minDateTime.Subtract(TimeSpan.FromMinutes(xRangeMinutes));

            //setup the x axis scaling/format
            zedGraphControl1.GraphPane.XAxis.Scale.MinAuto = false;
            zedGraphControl1.GraphPane.XAxis.Scale.Min = minDateTime.ToOADate();
            zedGraphControl1.GraphPane.XAxis.Scale.MaxAuto = false;
            zedGraphControl1.GraphPane.XAxis.Scale.MaxGrace = 0.01;
            zedGraphControl1.GraphPane.XAxis.Scale.Max = maxDateTime.ToOADate();
            
            zedGraphControl1.GraphPane.XAxis.Scale.FormatAuto = false;
            zedGraphControl1.GraphPane.XAxis.Scale.Format = "G";

            //setup the y axis scaling/format
            zedGraphControl1.GraphPane.YAxis.Scale.MinAuto = true;
            zedGraphControl1.GraphPane.YAxis.Scale.MaxAuto = true;
            zedGraphControl1.GraphPane.YAxis.Scale.MagAuto = false;
            zedGraphControl1.GraphPane.YAxis.Scale.FormatAuto = false;
            zedGraphControl1.GraphPane.YAxis.Scale.IsUseTenPower = false;
            zedGraphControl1.GraphPane.YAxis.Scale.Format = "f8";

            //setup the y2 axis scaling
            zedGraphControl1.GraphPane.Y2Axis.Scale.MinAuto = true;
            zedGraphControl1.GraphPane.Y2Axis.Scale.MaxAuto = true;
        }

        private delegate void GraphAxisChangeAndRefreshDelegate(bool axisChange, bool refresh);
        /**
         * Call ZedGraph's axischange and/or refresh methods (thread-safe).  
         */
        private void GraphAxisChangeAndRefresh(bool axisChange, bool refresh)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new GraphAxisChangeAndRefreshDelegate(GraphAxisChangeAndRefresh), new object[] { axisChange, refresh });
            }
            else
            {
                if (axisChange) zedGraphControl1.AxisChange();
                if (refresh) zedGraphControl1.Refresh();
            }
        }

        /**
         * Button to set the graph X range to 15 minutes.  
         */
        private void xScale15MinutesButton_Click(object sender, EventArgs e)
        {
            xRangeMinutes = 15;
            UpdateGraphXAxisForTrailingMin();
            GraphAxisChangeAndRefresh(true, true);
        }

        private void xScale1HourButton_Click(object sender, EventArgs e)
        {
            xRangeMinutes = 60;
            UpdateGraphXAxisForTrailingMin();
            GraphAxisChangeAndRefresh(true, true);
        }

        private void xScale3HourButton_Click(object sender, EventArgs e)
        {
            xRangeMinutes = 60*3;
            UpdateGraphXAxisForTrailingMin();
            GraphAxisChangeAndRefresh(true, true);
        }

        /**
         * Custom X range selection for the graph.  
         */
        private void xScaleComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (xScaleComboBox.SelectedItem == null)
            {
                SetStatusMessage("Invalid X scale combo box selection", "", true);
                return;
            }
            string hourStr = xScaleComboBox.SelectedItem.ToString();
            if (hourStr != "")
            {
                int hour = int.Parse(hourStr.Split(new char[] { ' ' })[0]);

                xRangeMinutes = 60 * hour;
                UpdateGraphXAxisForTrailingMin();
                GraphAxisChangeAndRefresh(true, true);
            }
        }

        /**
         * Button handler to manually retrieve the Cryptsy coin balances and update the GUI.  
         */
        private void updateBalancesButton_Click(object sender, EventArgs e)
        {
            double btc, doge;
            CryptsyAPI.APIMethods.CryptsyGetInfo(out btc, out doge, SetProgress);
            UpdateBalances(btc, doge);
        }

        /**
         * Edit program settings in a dialog window, including public/secret API keys.  
         */
        private void settingsButton_Click(object sender, EventArgs e)
        {
            SettingsDialog dlg = new SettingsDialog();
            if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                updatePeriodSeconds = Properties.Settings.Default.updatePeriodSeconds;
            }
        }

        private void buyDogeButton_Click(object sender, EventArgs e)
        {
            try
            {
                double buyAmount = double.Parse(buyAmountDogeTextBox.Text);
                double buyPrice = double.Parse(buyPriceDogeTextBox.Text);

                if (MessageBox.Show(
                    string.Format("Buy {0:0.00000000} DOGE @ {1:0.00000000} BTC each?", buyAmount, buyPrice), 
                    "Confirm Buy", 
                    MessageBoxButtons.OKCancel, 
                    MessageBoxIcon.Question) 
                    == System.Windows.Forms.DialogResult.OK)
                {
                    CryptsyAPI.CreateOrder createOrderResult = CryptsyAPI.APIMethods.CryptsyCreateOrderDOGE(true, buyAmount, buyPrice, SetProgress);
                    SetStatusMessage("Buy Success: " + createOrderResult.moreinfo, "", true);

                    double btc, doge;
                    CryptsyAPI.APIMethods.CryptsyGetInfo(out btc, out doge, SetProgress);
                    UpdateBalances(btc, doge);
                }
            }
            catch (Exception ex)
            {
                SetStatusMessage("Buy Error: " + ex.Message, ex.ToString(), true);
            }
        }

        private void sellDOGEButton_Click(object sender, EventArgs e)
        {
            try
            {
                double sellAmount = double.Parse(sellAmountDogeTextBox.Text);
                double sellPrice = double.Parse(sellPriceDogeTextBox.Text);

                if (MessageBox.Show(
                    string.Format("Sell {0:0.00000000} DOGE @ {1:0.00000000} BTC each?", sellAmount, sellPrice),
                    "Confirm Sell",
                    MessageBoxButtons.OKCancel,
                    MessageBoxIcon.Question)
                    == System.Windows.Forms.DialogResult.OK)
                {
                    CryptsyAPI.CreateOrder createOrderResult = CryptsyAPI.APIMethods.CryptsyCreateOrderDOGE(false, sellAmount, sellPrice, SetProgress);
                    SetStatusMessage("Sell Success: " + createOrderResult.moreinfo, "", true);

                    double btc, doge;
                    CryptsyAPI.APIMethods.CryptsyGetInfo(out btc, out doge, SetProgress);
                    UpdateBalances(btc, doge);
                }
            }
            catch (Exception ex)
            {
                SetStatusMessage("Sell Error: " + ex.Message, ex.ToString(), true);
            }
        }

        private void buyValuesChanged(object sender, EventArgs e)
        {
            try
            {
                double buyAmount = double.Parse(buyAmountDogeTextBox.Text);
                double buyPrice = double.Parse(buyPriceDogeTextBox.Text);

                double totalBTC = buyPrice * buyAmount;
                double feeBTC = btcDogeFee / 100 * totalBTC;
                double netTotalBTC = totalBTC + feeBTC;

                buyTotalLabel.Text = string.Format("{0:0.00000000} [BTC]", totalBTC);
                buyFeeLabel.Text = string.Format("{0:0.00000000} [BTC]", feeBTC);
                buyNetTotalLabel.Text = string.Format("{0:0.00000000} [BTC]", netTotalBTC);

                if (!double.IsNaN(btcAvailable))
                {
                    if (netTotalBTC > btcAvailable) buyNetTotalLabel.BackColor = Color.Red;
                    else buyNetTotalLabel.BackColor = SystemColors.Control;
                }
            }
            catch (Exception ex)
            {
                SetStatusMessage("Could not update buy totals: " + ex.Message, ex.ToString(), true);
                buyTotalLabel.Text = "n/a";
                buyFeeLabel.Text = "n/a";
                buyNetTotalLabel.Text = "n/a";
            }
        }

        private void sellValuesChanged(object sender, EventArgs e)
        {
            try
            {
                double sellAmount = double.Parse(sellAmountDogeTextBox.Text);
                double sellPrice = double.Parse(sellPriceDogeTextBox.Text);

                double totalBTC = sellPrice * sellAmount;
                double feeBTC = dogeBTCFee / 100 * totalBTC;
                double netTotalBTC = totalBTC + feeBTC;

                sellTotalLabel.Text = string.Format("{0:0.00000000} [BTC]", totalBTC);
                sellFeeLabel.Text = string.Format("{0:0.00000000} [BTC]", feeBTC);
                sellNetTotalLabel.Text = string.Format("{0:0.00000000} [BTC]", netTotalBTC);

                if (!double.IsNaN(dogeAvailable))
                {
                    if (sellAmount > dogeAvailable) sellAmountDogeTextBox.BackColor = Color.Red;
                    else sellAmountDogeTextBox.BackColor = SystemColors.Window;
                }
            }
            catch (Exception ex)
            {
                SetStatusMessage("Could not update sell totals: " + ex.Message, ex.ToString(), true);
                sellTotalLabel.Text = "n/a";
                sellFeeLabel.Text = "n/a";
                sellNetTotalLabel.Text = "n/a";
            }
        }

    }
}
